package entry

import (
	"bytes"
	"fmt"
	"html/template"
	"io"
	"math"
	"net/url"
	"regexp"
	"strconv"
	"time"

	"github.com/getkin/kin-openapi/openapi3"
)

var itemTemplate = `# related-url:{{.Url}} created-time:{{.Created.Format "2006-01-02T15:04:05Z07:00"}} error-type:{{.ErrorType}} error-count:{{.Count}}`
var commaMatcher = regexp.MustCompile(`# related-url:(.*) created-time:(.*) error-type:\[(\d+)\](.*) error-count:(\d+)`)
var temp *template.Template

type LogEntry struct {
	Url        string
	Method     string     `json:",omitempty"`
	Protocol   string     `json:",omitempty"`
	Client     string     `json:",omitempty"` //client ip
	StatusCode int        `json:",omitempty"`
	Legal      bool       `json:",omitempty"`
	Query      url.Values `json:",omitempty"`
	//OpenAPI参数错误
	ParameterErrors map[int]*ParameterError `json:",omitempty"`
	//
	ErrorType EntryError `json:",omitempty"`
	Created   time.Time
	Count     int64 //used for stat in rule
	Notified  bool
}

type LogEntryResponse struct {
	Entry *LogEntry
	Error error
}

func (l *LogEntry) DetectErrorByDef(def *openapi3.Operation) {
	for k, v := range def.Parameters {
		var err ParameterError
		if v.Value.In == "query" {
			submited := l.Query.Get(v.Value.Name)
			if v.Value.Required && submited == "" {
				err.Required = true
			}
			if v.Value.Schema != nil {
				var schema = v.Value.Schema.Value
				var values = l.Query[v.Value.Name]
				if submited != "" {
					var inEnum = false
					for _, vInEnum := range schema.Enum {
						if submited == fmt.Sprint(vInEnum) {
							inEnum = true
						}
					}
					if schema.Enum != nil && !inEnum {
						err.Enum = true
					}
					if schema.UniqueItems {
						var checker = make(map[string]bool)
						hasRepeated := false
						for _, v := range values {
							if checker[v] {
								hasRepeated = true
								break
							} else {
								checker[v] = true
							}
						}
						if hasRepeated {
							err.UniqueItems = true
						}
					}
					if schema.Deprecated {
						err.Deprecated = true
					}
					num, _ := strconv.ParseFloat(submited, 64)
					if schema.Min != nil && ((num < *schema.Min) || ((num == *schema.Min) && schema.ExclusiveMin)) {
						err.Min = true
					}
					if schema.Max != nil && ((num > *schema.Max) || ((num == *schema.Max) && schema.ExclusiveMax)) {
						err.Min = true
					}
					if schema.MultipleOf != nil && math.Mod(num, *schema.MultipleOf) != 0 {
						err.MultipleOf = true
					}
					if len(submited) < int(schema.MinLength) {
						err.MinLength = true
					}
					if schema.MaxLength != nil && len(submited) > int(*schema.MaxLength) {
						err.MinLength = true
					}
					if reg, er := regexp.Compile(schema.Pattern); er == nil {
						if !reg.MatchString(submited) {
							err.Pattern = true
						}
					}
					if len(values) < int(schema.MinItems) {
						err.MinItems = true
					}
					if schema.MaxItems != nil && len(values) > int(*schema.MaxItems) {
						err.MaxItems = true
					}
				} else {
					// if !schema.AllowEmptyValue && len(values) == 1 {
					// 	err.AllowEmptyValue = true
					// }
					if !schema.Nullable && len(values) == 0 {
						err.Nullable = true
					}
				}
			}
		} else if v.Value.In == "path" {
			//TODO
		}
		if err.HasError() {
			err.In = v.Value.In
			err.Name = v.Value.Name
			if l.ParameterErrors == nil {
				l.ParameterErrors = make(map[int]*ParameterError)
			}
			l.ParameterErrors[k] = &err
		}
	}
	if l.ParameterErrors == nil {
		l.ErrorType = Legal
	}
	//TODO 因为日志中一般没有cookie、header等参数作为日志，暂时不实现其他参数的判断
}

func (e *LogEntry) FillByRecord(recs []string) {
	if len(recs) > 1 {
		e.Url = recs[1]
	}
	if len(recs) > 2 {
		created, err := time.Parse(time.RFC3339, recs[2])
		if err == nil {
			e.Created = created
		}
	}
	if len(recs) > 3 {
		code, err := strconv.Atoi(recs[3])
		if err == nil {
			e.ErrorType = EntryError(code)
		}
	}
	if len(recs) > 5 {
		e.Count, _ = strconv.ParseInt(recs[5], 10, 64)
	}
}

func (e *LogEntry) String() string {
	return fmt.Sprintf("url:%s,at:%v,error:%v", e.Url, e.Created.Local(), e.ErrorType)
}

func (e *LogEntry) Description() string {
	var buf bytes.Buffer
	e.WriteTo(&buf, "")
	return buf.String()
}

type ParameterError struct {
	Type   bool `json:"type,omitempty" yaml:"type,omitempty"`
	Format bool `json:"format,omitempty" yaml:"format,omitempty"`
	Enum   bool `json:"enum,omitempty" yaml:"enum,omitempty"`

	Name     string
	In       string
	Required bool `json:"required,omitempty" yaml:"required,omitempty"`

	// Array-related, here for struct compactness
	UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
	// Number-related, here for struct compactness
	ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"`
	ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"`
	// Properties
	Nullable        bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
	AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
	Deprecated      bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`

	// Number
	Min        bool `json:"minimum,omitempty" yaml:"minimum,omitempty"`
	Max        bool `json:"maximum,omitempty" yaml:"maximum,omitempty"`
	MultipleOf bool `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"`

	// String
	MinLength bool `json:"minLength,omitempty" yaml:"minLength,omitempty"`
	MaxLength bool `json:"maxLength,omitempty" yaml:"maxLength,omitempty"`
	Pattern   bool `json:"pattern,omitempty" yaml:"pattern,omitempty"`

	// Array
	MinItems bool `json:"minItems,omitempty" yaml:"minItems,omitempty"`
	MaxItems bool `json:"maxItems,omitempty" yaml:"maxItems,omitempty"`

	// Object TODO
	// ObjectRequired              bool `json:"required,omitempty" yaml:"required,omitempty"`
	// MinProps                    bool `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
	// MaxProps                    bool `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
	// AdditionalPropertiesAllowed bool `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // In this order...
}

func (e *ParameterError) HasError() bool {
	return e.Type ||
		e.Format ||
		e.Enum ||
		e.Required ||
		e.UniqueItems ||
		e.ExclusiveMin ||
		e.ExclusiveMax ||
		e.Nullable ||
		e.AllowEmptyValue ||
		e.Deprecated ||
		e.Min ||
		e.Max ||
		e.MultipleOf ||
		e.MinLength ||
		e.MaxLength ||
		e.Pattern ||
		e.MinItems ||
		e.MaxItems
}

func (l *LogEntry) SeperatePath() {
	u, err := url.Parse(l.Url)
	if err == nil {
		l.Url = u.Path
		l.Query = u.Query()
	}
}

func (e *LogEntry) WriteTo(w io.Writer, newLine string) error {
	if temp == nil {
		temp, _ = template.New("iptable").Parse(itemTemplate)
	}
	err := temp.Execute(w, e)
	if err == nil && newLine != "" {
		w.Write([]byte{'\n'})
		w.Write([]byte(newLine))
	}
	return err
}

func ParseString(line string) *LogEntry {
	matched := commaMatcher.FindStringSubmatch(line)
	if len(matched) > 0 {
		currentEntry := new(LogEntry)
		currentEntry.FillByRecord(matched)
		return currentEntry
	}
	return nil
}
